// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package editor includes editor related manipulations.
package editor

import (
	"bytes"
	"encoding/json"
	"errors"
	"go/token"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"
	"unicode"

	"chainmaker.org/chainmaker/smarteditor/common"
	"chainmaker.org/chainmaker/smarteditor/conf"
	"chainmaker.org/chainmaker/smarteditor/file"
	"chainmaker.org/chainmaker/smarteditor/gocode"
	"chainmaker.org/chainmaker/smarteditor/logger"
	"chainmaker.org/chainmaker/smarteditor/session"
	"chainmaker.org/chainmaker/smarteditor/util"
	"github.com/88250/gulu"
)

// Logger.
var log = logger.Get()

//autocomplete result
type autocompleteResult struct {
	Cls  string `json:"class"`
	Pkg  string `json:"package"`
	Name string `json:"name"`
	T    string `json:"type"`
}

// AutocompleteHandler handles request of code autocompletion.
func AutocompleteHandler(w http.ResponseWriter, r *http.Request) {

	var args map[string]interface{}
	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		http.Error(w, "Forbidden", http.StatusInternalServerError)
		return
	}
	path := args["path"].(string)
	code := args["code"].(string)
	line := int(args["cursorLine"].(float64))
	ch := int(args["cursorCh"].(float64))

	_, err := session.GetUidFromSession(r)
	if err != nil {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}
	if gulu.Go.IsAPI(path) || util.BelongsGoPath(path) {
		http.Error(w, "has no auth", http.StatusBadRequest)
		return
	}
	err = ioutil.WriteFile(path, []byte(code), 0644)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	lineStartIndex, offset := getCursorLineStartIndexAndOffset(code, line, ch)
	lineText := code[lineStartIndex:offset]
	log.Debugf("autocomplete handler. line:%s, line offset:%d,lineStartIndex:%d, offset: %d", lineText, ch, lineStartIndex, offset)
	if !shouldAutocomplete(lineText, ch) {
		http.Error(w, "illegal invoke", http.StatusBadRequest)
		return
	}

	gocodeStart := time.Now()
	autoCompleteResponse, err := gocode.AutoComplete(path, filepath.Dir(path), []byte(code), offset, common.GoCodeServerAddr)
	if nil != err {
		log.Errorf("autocomplete handler, run autocomplete fail. err:%v", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	log.Debugf("time cost: autocomplete handler, go code. (cost:%s)", time.Since(gocodeStart))

	autoCompleteResponse = replaceOffset(addAutoComplete(lineText, autoCompleteResponse, ch), ch)
	w.Header().Set("Content-Type", "application/json")
	util.Write(w, autoCompleteResponse)
}

// GetExprInfoHandler handles request of getting expression information.
func GetExprInfoHandler(w http.ResponseWriter, r *http.Request) {
	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetResult(w, r, result)

	session, _ := session.HTTPSession.Get(r, session.CookieName)
	uid := session.Values["uid"].(string)

	var args map[string]interface{}
	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		log.Error(err)
		http.Error(w, err.Error(), http.StatusInternalServerError)

		return
	}

	path := args["path"].(string)
	curDir := filepath.Dir(path)
	filename := filepath.Base(path)

	fout, err := os.Create(path)

	if nil != err {
		log.Error(err)
		result.Code = -1

		return
	}

	code := args["code"].(string)
	fout.WriteString(code)

	if err := fout.Close(); nil != err {
		log.Error(err)
		result.Code = -1

		return
	}

	line := int(args["cursorLine"].(float64))
	ch := int(args["cursorCh"].(float64))

	_, offset := getCursorLineStartIndexAndOffset(code, line, ch)

	log.Debugf("offset [%d]", offset)

	ideStub := gulu.Go.GetExecutableInGOBIN("gotools")
	argv := []string{"types", "-pos", filename + ":" + strconv.Itoa(offset), "-info", "."}
	cmd := exec.Command(ideStub, argv...)
	cmd.Dir = curDir

	setCmdEnv(cmd, uid)

	output, err := cmd.CombinedOutput()
	if nil != err {
		log.Error(err)
		http.Error(w, err.Error(), http.StatusInternalServerError)

		return
	}

	exprInfo := strings.TrimSpace(string(output))
	if "" == exprInfo {
		result.Code = -1

		return
	}

	result.Data = exprInfo
}

// FindDeclarationHandler handles request of finding declaration.
func FindDeclarationHandler(w http.ResponseWriter, r *http.Request) {
	session, _ := session.HTTPSession.Get(r, session.CookieName)
	if session.IsNew {
		log.Debugf("FindDeclarationHandler no cookie")
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	uid := session.Values["uid"].(string)

	var args map[string]interface{}
	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		log.Error(err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	path := args["path"].(string)
	code := args["code"].(string)

	curDir := filepath.Dir(path)
	filename := filepath.Base(path)

	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetResult(w, r, result)
	if !util.IsAPI(util.RuntimeGoPath, path, conf.GoModulePath()) {
		err := ioutil.WriteFile(path, []byte(code), 0644)
		if err != nil {
			log.Error(err)
			result.Code = -1
			return
		}
	}

	line := int(args["cursorLine"].(float64))
	ch := int(args["cursorCh"].(float64))

	_, offset := getCursorLineStartIndexAndOffset(code, line, ch)

	log.Debugf("[FindDeclarationHandler] fileName[%s] offset [%d]", filename, offset)

	ideStub := gulu.Go.GetExecutableInGOBIN("gotools")
	argv := []string{"types", "-pos", filename + ":" + strconv.Itoa(offset), "-def", "."}
	cmd := exec.Command(ideStub, argv...)
	cmd.Dir = curDir

	setCmdEnv(cmd, uid)

	output, err := cmd.CombinedOutput()
	if nil != err {
		log.Errorf("[FindDeclarationHandler] gotools run fail. output:%s, err:%s", string(output), err)
		result.Code = -1
		return
	}

	found := strings.TrimSpace(string(output))
	if "" == found {
		result.Code = -1
		return
	}

	part := found[:strings.LastIndex(found, ":")]
	cursorSep := strings.LastIndex(part, ":")
	path = found[:cursorSep]

	cursorLine, _ := strconv.Atoi(found[cursorSep+1 : strings.LastIndex(found, ":")])
	cursorCh, _ := strconv.Atoi(found[strings.LastIndex(found, ":")+1:])

	data := map[string]interface{}{}
	result.Data = &data

	data["path"] = filepath.ToSlash(path)
	data["cursorLine"] = cursorLine
	data["cursorCh"] = cursorCh
}

// FindUsagesHandler handles request of finding usages.
func FindUsagesHandler(w http.ResponseWriter, r *http.Request) {
	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetResult(w, r, result)

	session, _ := session.HTTPSession.Get(r, session.CookieName)
	if session.IsNew {
		http.Error(w, "Forbidden", http.StatusForbidden)

		return
	}
	uid := session.Values["uid"].(string)

	var args map[string]interface{}

	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		log.Error(err)
		http.Error(w, err.Error(), http.StatusInternalServerError)

		return
	}

	filePath := args["path"].(string)
	curDir := filepath.Dir(filePath)
	filename := filepath.Base(filePath)

	fout, err := os.Create(filePath)

	if nil != err {
		log.Error(err)
		result.Code = -1

		return
	}

	code := args["code"].(string)
	fout.WriteString(code)

	if err := fout.Close(); nil != err {
		log.Error(err)
		result.Code = -1

		return
	}

	line := int(args["cursorLine"].(float64))
	ch := int(args["cursorCh"].(float64))

	_, offset := getCursorLineStartIndexAndOffset(code, line, ch)
	log.Debugf("offset [%d]", offset)

	ideStub := gulu.Go.GetExecutableInGOBIN("gotools")
	argv := []string{"types", "-pos", filename + ":" + strconv.Itoa(offset), "-use", "."}
	cmd := exec.Command(ideStub, argv...)
	cmd.Dir = curDir

	setCmdEnv(cmd, uid)

	output, err := cmd.CombinedOutput()
	if nil != err {
		log.Error(err)
		http.Error(w, err.Error(), http.StatusInternalServerError)

		return
	}

	out := strings.TrimSpace(string(output))
	if "" == out {
		result.Code = -1

		return
	}

	founds := strings.Split(out, "\n")
	usages := []*file.Snippet{}
	for _, found := range founds {
		found = strings.TrimSpace(found)

		part := found[:strings.LastIndex(found, ":")]
		cursorSep := strings.LastIndex(part, ":")
		path := filepath.ToSlash(found[:cursorSep])
		cursorLine, _ := strconv.Atoi(found[cursorSep+1 : strings.LastIndex(found, ":")])
		cursorCh, _ := strconv.Atoi(found[strings.LastIndex(found, ":")+1:])

		usage := &file.Snippet{Path: path, Line: cursorLine, Ch: cursorCh, Contents: []string{""}}
		usages = append(usages, usage)
	}

	result.Data = usages
}

// getCursorOffset calculates the cursor offset.
//
// line is the line number, starts with 0 that means the first line
// ch is the column number, starts with 0 that means the first column
func getCursorLineStartIndexAndOffset(code string, line, ch int) (int, int) {
	lineStartIndex := 0
	offset := 0
	newLineCount := 0
	for index := range code {
		if newLineCount == line {
			break
		}
		if code[index] == '\n' {
			offset = index + 1
			newLineCount++
			lineStartIndex = index + 1
		}
	}
	remainText := code[lineStartIndex:]
	index := 0
	for a, _ := range remainText {
		index++
		if index == ch {
			offset += a
			offset = offset + 1
			break
		}
	}
	return lineStartIndex, offset
}

func setCmdEnv(cmd *exec.Cmd, userID string) {
	//userWorkspace := conf.GetUserWorkspace(userID)
	cache, err := os.UserCacheDir()
	if nil != err {
		log.Warnf("Get user cache dir failed [" + err.Error() + "]")
		cache = os.TempDir()
	}
	cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
	cmd.Env = append(cmd.Env,
		"GOPATH="+os.Getenv("GOPATH"), //userWorkspace,
		"GOPROXY=https://goproxy.cn",
		"GO111MODULE=on",
		"GOOS="+runtime.GOOS,
		"GOARCH="+runtime.GOARCH,
		"GOROOT="+runtime.GOROOT(),
		"GOCACHE="+cache,
		"GONOSUMDB=",
		"GOPRIVATE=",
		"PATH="+runtime.GOROOT()+"/bin:"+os.Getenv("PATH"))
	// if gulu.OS.IsWindows() {
	// 	// FIXME: for some weird issues on Windows, such as:
	// 	// The requested service provider could not be loaded or initialized.
	// 	environ := os.Environ()
	// 	for i := range environ {
	// 		if strings.HasPrefix(environ[i], "GOROOT") {
	// 			environ[i] = "GOROOT=" + runtime.GOROOT()
	// 		}
	// 	}
	// 	cmd.Env = append(cmd.Env, environ...)
	// } else {
	// 	// 编译链接时找不到依赖的动态库 https://github.com/b3log/wide/issues/352
	// 	cmd.Env = append(cmd.Env, "LD_LIBRARY_PATH="+os.Getenv("LD_LIBRARY_PATH"))
	// }
	log.Debugf("[FindDeclarationHandler] setCmdEnv %v", cmd.Env)
}

func shouldAutocomplete(line string, lineOffset int) bool {
	//case 1: empty file
	if lineOffset == 0 {
		return false
	}
	//case 2: one line comment
	if strings.HasPrefix(strings.TrimSpace(line), "//") {
		return false
	}

	var chOfOffset rune
	index := 0
	for _, c := range line {
		if index == lineOffset {
			break
		}
		chOfOffset = c
		index++
	}
	if string(chOfOffset) == "." {
		return true
	}

	//case 3: symbol or punctuation
	if unicode.IsSymbol(chOfOffset) || unicode.IsPunct(chOfOffset) {
		return false
	}
	// case 4: space
	if unicode.IsSpace(chOfOffset) {
		return false
	}
	return true
}

func keywordAutocomplete(line string) (string, error) {
	results := make([]autocompleteResult, 0)
	trimSpaceLine := strings.TrimSpace(line)
	lastSpaceIndex := strings.LastIndex(trimSpaceLine, " ")
	if lastSpaceIndex == -1 {
		lastSpaceIndex = 0
	} else {
		lastSpaceIndex = lastSpaceIndex + 1
	}
	lastCode := trimSpaceLine[lastSpaceIndex:]
	for t := token.BREAK; t <= token.VAR; t++ {
		if t.String() != lastCode && strings.HasPrefix(t.String(), lastCode) {
			results = append(results, autocompleteResult{Name: t.String(), Cls: "var", Pkg: "", T: ""})
		}
	}
	if len(results) == 0 {
		return "", errors.New("empty")
	}
	marshal, _ := json.Marshal(results)
	return string(marshal[1 : len(marshal)-1]), nil
}

func addAutoComplete(lineText string, output []byte, offset int) []byte {
	if autocomplete, err := keywordAutocomplete(lineText); err == nil {
		split := ","
		if strings.TrimSpace(string(output)) == "null" {
			split = ""
			output = []byte("[" + strconv.Itoa(offset) + ",[]] ")
		}
		length := len(output)
		b := bytes.Buffer{}
		b.Write(output[0 : length-3])
		b.WriteString(split)
		b.WriteString(autocomplete)
		b.Write(output[length-3:])
		output = b.Bytes()
	}
	return output
}

func replaceOffset(output []byte, offset int) []byte {
	if strings.TrimSpace(string(output)) != "null" {
		b := bytes.Buffer{}
		b.Write(output[:1])
		b.WriteString(strconv.Itoa(offset))
		index := strings.Index(string(output), ",")
		b.Write(output[index:])
		return b.Bytes()
	} else {
		return output
	}
}
